﻿// <copyright file="GameProvider.cs" company="Ecole Normale Supérieure">
// Released under GPLv2
// </copyright>
// <summary>Contains the GameProvider provider</summary>

namespace Sqwarea.Persistence.Azure
{
    using System;
    using System.Collections.Generic;
    using System.Data.Services.Client;
    using System.Drawing;
    using System.Globalization;
    using System.Linq;
    using System.Threading;
    using Lokad;
    using Lokad.Cloud.Storage;
    using Sqwarea.Persistence.Exceptions;

    /// <summary>
    /// Provider for most of the game-related data stored on the cloud
    /// </summary>
    public class GameProvider : IGameProvider
    {
        // initialisation

        /// <summary>
        /// Name of the Queue in which the Attacks will enventually end up.
        /// </summary>
        public const string AttackQueueName = "sqware-attack-queue";

        /// <summary>
        /// Date Format used when converting Dates to strings
        /// </summary>
        public const string GameDateFormat = "yyyy-MM-dd-HH-mm-ss-ffff";

        /// <summary>
        /// Size of the "big square of square" stored in a single partition
        /// </summary>
        public const int SizeOfSubZones = 20;

        /// <summary>
        /// Name of the table Storing the map
        /// </summary>
        public const string SquareTableName = "sqwareasquaretable";

        /// <summary>
        /// Name of the table storing attacks incoming on a square
        /// </summary>
        public const string IncomingAttackTableName = "sqwareaincommingattacktable";

        /// <summary>
        /// Name of the table storing attacks ongoing from each king
        /// </summary>
        public const string OngoingAttackTableName = "sqwareaongoingattacktable";

        /// <summary>
        /// Name of the container containing blobs for kings
        /// </summary>
        public const string KingBlobContainerName = "sqwareaongoingattacktable";

        /// <summary>
        /// blob provider
        /// </summary>
        private readonly IBlobStorageProvider _blobProvider;

        /// <summary>
        /// table provider
        /// </summary>
        private readonly ITableStorageProvider _tableProvider;

        /// <summary>
        /// queue provider
        /// </summary>
        private readonly IQueueStorageProvider _queueProvider;

        /// <summary>
        /// DelayedQueue used to store the Attacks to be handled
        /// </summary>
        private readonly DelayedQueue _delayedQueue;

        /// <summary>
        /// Initializes a new instance of the GameProvider class.
        /// </summary>
        /// <param name="blobProvider"> a blob provider</param>
        /// <param name="tableProvider"> a table provider</param>
        /// <param name="queueProvider"> a queue provider</param>
        [CLSCompliantAttribute(false)]
        public GameProvider(IBlobStorageProvider blobProvider, ITableStorageProvider tableProvider, IQueueStorageProvider queueProvider)
        {
            if (null == blobProvider)
            {
                throw new ArgumentNullException("blobProvider", "blobProvider must not be null");
            }

            if (null == tableProvider)
            {
                throw new ArgumentNullException("tableProvider", "tableProvider must not be null");
            }

            if (null == queueProvider)
            {
                throw new ArgumentNullException("queueProvider", "queueProvider must not be null");
            }

            _blobProvider = blobProvider;
            _tableProvider = tableProvider;
            _queueProvider = queueProvider;
            _delayedQueue = new DelayedQueue(blobProvider);

            ////_blobProvider.CreateContainer(KingBlobContainerName);
            _tableProvider.CreateTable(SquareTableName);
            _tableProvider.CreateTable(IncomingAttackTableName);
            _tableProvider.CreateTable(OngoingAttackTableName);
        }

        // Getting functions

        /// <summary>
        /// Gets the next King Id.
        /// Pop the top element in the King Id queue.
        /// </summary>
        /// <returns>Next king Id</returns>
        public long GetNextKingId()
        {
            Maybe<long> nextKingId = _queueProvider.Get<long>(QueueProvider.NewKingQueueName, 1).FirstOrEmpty();
            if (!nextKingId.HasValue)
            {
                throw new PersistenceNoKingIdException("No more King Id in the queue");
            }

            _queueProvider.Delete(nextKingId.Value);
            return nextKingId.Value;
        }

        /// <summary>
        /// Get the object king corresponding to the kingId
        /// </summary>
        /// <param name="kingId">Id of the required king</param>
        /// <returns>See summary</returns>
        public King GetKingById(long kingId)
        {
            Maybe<King> king = _blobProvider.GetBlob(new KingName(kingId));
            if (!king.HasValue)
            {
                throw new PersistenceKingNotFoundException(kingId, "No King was found with this Id");
            }

            return king.Value;
        }

        /// <summary>
        /// Get the object square corresponding to the position position
        /// </summary>
        /// <param name="position">Position required</param>
        /// <returns>See summary</returns>
        public Square GetSquareByPosition(Point position)
        {
            var square = _tableProvider.Get<Square>(SquareTableName, this.GetPartitionKey(position), GetRowKey(position));
            if (square.HasValue)
            {
                var s = square.Value.Value;
                s.ETag = square.Value.ETag;
                return s;
            }

            return new Square(position) { ETag = "nothing" };
        }

        /// <summary>
        /// Get all the squares in a given rectangle 
        /// Take care of the fact that the given rectangle might overlay on many "big squares"
        /// </summary>
        /// <param name="rectangle">Rectangle delimiting the area required</param>
        /// <returns>A 2 dimentional array containing all the square of the requiered area relatively to the top left case 
        /// (return a default Square if nothing is stored at the selected adress)</returns>
        public Square[,] GetSquaresInRectangle(Rectangle rectangle)
        {
            if (rectangle.Left > rectangle.Right || rectangle.Top > rectangle.Bottom)
            {
                throw new PersistenceInvalidRectangleException(rectangle, "rectangle isn't valid, top must be \"smaller\" than Bottom...");
            }

            var result = new Square[rectangle.Height, rectangle.Width];

            var doneEvents = new List<ManualResetEvent>();

            for (int x = Tools.Div(rectangle.Left, SizeOfSubZones) * SizeOfSubZones; x < rectangle.Right; x += SizeOfSubZones)
            {
                for (int y = Tools.Div(rectangle.Top, SizeOfSubZones) * SizeOfSubZones; y < rectangle.Bottom; y += SizeOfSubZones)
                {
                    var resetEvent = new ManualResetEvent(false);
                    int x1 = x;
                    int y1 = y;

                    ThreadPool.QueueUserWorkItem(
                            threadContext =>
                            {
                                var squares = _tableProvider.Get<Square>(
                                    SquareTableName,
                                    this.GetPartitionKey(new Point(x1, y1)));

                                foreach (Square square in squares
                                    .Select(cloudEntity =>
                                            {
                                                var s = cloudEntity.Value;
                                                s.ETag = cloudEntity.ETag;
                                                return s;
                                            })
                                    .Where(square => square.Position.X < rectangle.Right &&
                                                     square.Position.X >= rectangle.Left &&
                                                     square.Position.Y >= rectangle.Top &&
                                                     square.Position.Y < rectangle.Bottom))
                                {
                                    result[square.Position.Y - rectangle.Top, square.Position.X - rectangle.Left] =
                                        square;
                                }

                                resetEvent.Set();
                            });

                    doneEvents.Add(resetEvent);
                }
            }

            if (doneEvents.Count() != 0)
            {
                WaitHandle.WaitAll(doneEvents.ToArray());
            }

            for (int x = 0; x < rectangle.Width; x++)
            {
                for (int y = 0; y < rectangle.Height; y++)
                {
                    if (null == result[y, x])
                    {
                        result[y, x] = new Square(new Point(x + rectangle.Left, y + rectangle.Top)) { ETag = "nothing" };
                    }
                }
            }

            return result;
        }

        /// <summary>
        /// Get the square at point and the surrondings ones, which has the same partition key
        /// </summary>
        /// <param name="point">Point that must be retrieved</param>
        /// <returns>An IEnumerable containing all square surronding point</returns>
        public IEnumerable<Square> GetSquaresInPartition(Point point)
        {
            var squares = _tableProvider.Get<Square>(SquareTableName, this.GetPartitionKey(point));
            return squares.Select(cloudEntity =>
                               {
                                   var s = cloudEntity.Value;
                                   s.ETag = cloudEntity.ETag;
                                   return s;
                               });
        }

        /// <summary>
        /// Gets all squares of the map
        /// Only used by ConnexityTotalChecker.
        /// NEVER USE IT FOR ANYTHING ELSE !
        /// </summary>
        /// <returns>All squares of the map</returns>
        public IEnumerable<Square> GetAllSquares()
        {
            var squares = _tableProvider.Get<Square>(SquareTableName);
            return squares.Select(cloudEntity =>
            {
                var s = cloudEntity.Value;
                s.ETag = cloudEntity.ETag;
                return s;
            });
        }

        /// <summary>
        /// Gets the incoming attacks to kingId user (i.e. attacks against this king's squares).
        /// Not realy precise. If the king get possession of a new square he won't see the attacks launched before
        /// he got the square, and the previous owner of the square will continue seeing attacks launched on
        /// square he no longer owned if he had the territory when the attack was lauched
        /// </summary>
        /// <param name="kingId">Id of the required king</param>
        /// <returns>See summary</returns>
        public IEnumerable<Attack> GetIncomingAttacks(long kingId)
        {
            var attacks = _tableProvider.Get<Attack>(IncomingAttackTableName, kingId.ToString(Tools.NumberFormat));
            return from attack in attacks
                   where attack.Value.ArrivalDate.CompareTo(DateTime.UtcNow) > 0
                   select attack.Value;
        }

        /// <summary>
        /// Gets the ongoing attacks on kingId user (i.e. attacks sended by this king).
        /// </summary>
        /// <param name="kingId">Id of the required king</param>
        /// <returns>See summary</returns>
        public IEnumerable<Attack> GetOngoingAttacks(long kingId)
        {
            var attacks = _tableProvider.Get<Attack>(OngoingAttackTableName, kingId.ToString(Tools.NumberFormat));
            return from attack in attacks
                   where attack.Value.ArrivalDate.CompareTo(DateTimeOffset.UtcNow) > 0
                   select attack.Value;
        }

        // Setting functions

        /// <summary>
        /// Update the information on the squares given in the IEnumerable
        /// </summary>
        /// <param name="squares">Squares to update (containing position)</param>
        public void UpdateSquares(IEnumerable<Square> squares)
        {
            var cloudEntities =
                squares
                    .Where(x => null != x)
                    .Select(
                        square =>
                        new CloudEntity<Square>
                            {
                                Value = square,
                                PartitionKey = this.GetPartitionKey(square.Position),
                                RowKey = GetRowKey(square.Position)
                            });

            _tableProvider.Upsert(SquareTableName, cloudEntities);
        }

        /// <summary>
        /// Update the information on the square defined by its internal position
        /// </summary>
        /// <param name="square">Square to update (containing position)</param>
        public void UpdateSquare(Square square)
        {
            var squareEntity = new CloudEntity<Square>
                                   {
                                       Value = square,
                                       PartitionKey = this.GetPartitionKey(square.Position),
                                       RowKey = GetRowKey(square.Position)
                                   };

            CloudEntity<Square>[] squarelist = { squareEntity };
            _tableProvider.Upsert(SquareTableName, squarelist);
        }

        /// <summary>
        /// Atomic update of a Square
        /// </summary>
        /// <param name="position">The position of the square to update</param>
        /// <param name="updateFunction">Function that must be called on the square.
        ///                              If the function returns "null" then no changes are done
        /// </param>
        /// <returns>the updated Square</returns>
        public Square UpdateSquare(Point position, Func<Square, Square> updateFunction)
        {
            var squareEntity = _tableProvider.Get<Square>(SquareTableName, this.GetPartitionKey(position), GetRowKey(position));

            // if we found nothing in the table, then we add a default Square at this position add apply the UdpdateSquare method on it
            // This way, if the same "non-existing" square get updated twice sumulaneously, it get created if it still not exist, 
            // and then get atomicaly updated
            if (!squareEntity.HasValue)
            {
                var newSquareEntity = new CloudEntity<Square>
                                          {
                                              Value = new Square(position),
                                              PartitionKey = this.GetPartitionKey(position),
                                              RowKey = GetRowKey(position)
                                          };
                try
                {
                    _tableProvider.Insert(SquareTableName, new[] { newSquareEntity });
                }
                catch (DataServiceRequestException)
                {
                }

                return UpdateSquare(position, updateFunction);
            }

            // If we found something, then we apply change to the square and modify it, and then ask to change it
            // If it has already been changed then we try again
            squareEntity.Value.Value = updateFunction(squareEntity.Value.Value);

            // if the function returned null then we dont make any changes
            if (squareEntity.Value.Value == null)
            {
                return squareEntity.Value.Value;
            }

            try
            {
                _tableProvider.Update(SquareTableName, new[] { squareEntity.Value }, false);
                return squareEntity.Value.Value;
            }
            catch (DataServiceRequestException)
            {
                return UpdateSquare(position, updateFunction);
            }
        }

        /// <summary>
        /// Update the information on an existing king (defined by its id) or create a new king if the id is
        /// not referenced
        /// </summary>
        /// <param name="king">King to create or update (containing id)</param>
        public void UpdateKing(King king)
        {
            _blobProvider.PutBlob(new KingName(king.Id), king);
        }

        /// <summary>
        /// Atomic update of a King
        /// </summary>
        /// <param name="kingId">Id of the king to create or update</param>
        /// <param name="updateFunction">Function that must be called on the king</param>
        /// <returns>the updated King</returns>
        public King UpdateKing(long kingId, Func<King, King> updateFunction)
        {
            Func<Maybe<King>, King> func = maybeKing =>
                           {
                               if (maybeKing.HasValue)
                               {
                                   return updateFunction(maybeKing.Value);
                               }

                               throw new PersistenceKingNotFoundException();
                           };
            King result;
            _blobProvider.AtomicUpdate(new KingName(kingId), func, out result);
            return result;
        }

        /// <summary>
        /// Returns an IEnumerable that contains all kings. For everyday top kings sorting
        /// </summary>
        /// <returns>An object from which we can get all kings.</returns>
        public IEnumerable<King> GetAllKings()
        {
            IEnumerable<KingName> allKingsName = _blobProvider.List(new KingName());
            return from name in allKingsName select _blobProvider.GetBlob(name).Value;
        }

        /// <summary>
        /// Create a new attack
        /// </summary>
        /// <param name="attack">Attack to create</param>
        public void NewAttack(Attack attack)
        {
            var doneEvents = new ManualResetEvent[2];

            doneEvents[0] = new ManualResetEvent(false);
            ThreadPool.QueueUserWorkItem(
                a =>
                {
                    Square destination = GetSquareByPosition(attack.Destination);
                    if (destination.KingId != 0 && destination.KingId != attack.SenderId)
                    {
                        var incomingAttack = new CloudEntity<Attack>
                                                 {
                                                     Value = attack,
                                                     PartitionKey = destination.KingId.ToString(Tools.NumberFormat),
                                                     RowKey = DateToRandomString(attack.ArrivalDate)
                                                 };
                        CloudEntity<Attack>[] incomingAttackList = { incomingAttack };

                        _tableProvider.Insert(IncomingAttackTableName, incomingAttackList);
                    }

                    doneEvents[0].Set();
                });

            doneEvents[1] = new ManualResetEvent(false);
            ThreadPool.QueueUserWorkItem(
                a =>
                {
                    var ongoingAttack = new CloudEntity<Attack>
                                            {
                                                Value = attack,
                                                PartitionKey = attack.SenderId.ToString(Tools.NumberFormat),
                                                RowKey = DateToRandomString(attack.ArrivalDate)
                                            };

                    CloudEntity<Attack>[] ongoingAttackList = { ongoingAttack };
                    _tableProvider.Insert(OngoingAttackTableName, ongoingAttackList);
                    doneEvents[1].Set();
                });

            _delayedQueue.PutWithDelay(attack, attack.ArrivalDate, AttackQueueName);

            WaitHandle.WaitAll(doneEvents);
        }

        /// <summary>
        /// Allow a little bit of cleaning from times to times.
        /// </summary>
        /// <param name="kingIds">Kings that must be "cleaned"</param>
        public void ClearElapsedAttacks(IEnumerable<long> kingIds)
        {
            foreach (var kingId in kingIds)
            {
                var incomming = _tableProvider.Get<Attack>(IncomingAttackTableName, kingId.ToString(Tools.NumberFormat));
                var incommingKeys = from attack in incomming
                                    where attack.Value.ArrivalDate.CompareTo(DateTimeOffset.UtcNow) < 0
                                    select attack.RowKey;
                _tableProvider.Delete<Attack>(
                                                   IncomingAttackTableName,
                                                   kingId.ToString(Tools.NumberFormat),
                                                   incommingKeys);

                var ongoing = _tableProvider.Get<Attack>(OngoingAttackTableName, kingId.ToString(Tools.NumberFormat));
                var ongoingKeys = from attack in ongoing
                                  where attack.Value.ArrivalDate.CompareTo(DateTimeOffset.UtcNow) < 0
                                  select attack.RowKey;
                _tableProvider.Delete<Attack>(
                                                   OngoingAttackTableName,
                                                   kingId.ToString(Tools.NumberFormat),
                                                   ongoingKeys);
            }
        }

        /// <summary>
        /// return the Row key containing the Square having position point
        /// </summary>
        /// <param name="point">Position of the square</param>
        /// <returns>the row Key linked to this square</returns>
        private static string GetRowKey(Point point)
        {
            return point.ToAlphaString();
        }

        /// <summary>
        /// Transform a date into a string according to invariant culture format and the one defined in GameDateFormat,
        /// and Add 6 random caracters at the end to allow multiple attacks to occure at the same milisecond
        /// </summary>
        /// <param name="date">a date to transform into a string</param>
        /// <returns>a string depending on the entered Date and Lokad's string generator</returns>
        private static string DateToRandomString(DateTimeOffset date)
        {
            return date.ToString(GameDateFormat, CultureInfo.InvariantCulture) + "-" + Tools.RandId();
        }

        /// <summary>
        /// Class enabling to select a king on the blob storage (kind of strong typed key)
        /// </summary>
        private class KingName : BlobName<King>
        {
            /// <summary>
            /// Gets the kingId, which will be the name of the blob
            /// </summary>
            [Rank(0)]
            public readonly string KingId;

            /// <summary>
            /// Initializes a new instance of the KingName class.
            /// </summary>
            /// <param name="kingId">kingId of the king</param>
            public KingName(long kingId)
            {
                if (kingId <= 0)
                {
                    throw new ArgumentOutOfRangeException("kingId", "kingId must not be negative or null");
                }

                KingId = kingId.ToString(Tools.NumberFormat);
            }

            /// <summary>
            /// Initializes a new instance of the KingName class. Empty KingName Prefix for the GetAllKings funciton
            /// </summary>
            public KingName()
            {
                KingId = string.Empty;
            }

            /// <summary>
            /// Name of blob container
            /// </summary>
            public override string ContainerName
            {
                get { return KingBlobContainerName; }
            }
        }
    }
}